import * as url from 'url'

import utils from '../utils'
import protocols from './protocols'
import { camel2snakeJson, snake2camelJson, UniCloudError } from '../../shared'

class PaymentBase {
  constructor (options) {
    this.options = options

    if (!options.appId) throw new Error('appId required')
    if (!options.mchId) throw new Error('mchId required')
    if (!options.v3Key) throw new Error('v3Key required')
    if (!options.appPrivateKeyPath && !options.appPrivateKeyContent) throw new Error('missing appPrivateKeyPath or appPrivateKeyContent')
    if (!options.appCertPath && !options.appCertContent) throw new Error('missing appCertPath or appCertContent')
    this._protocols = protocols
    this.platformCertificate = [] // 平台证书
    this._baseURL = 'https://api.mch.weixin.qq.com'

    this._cert = options.appCertPath ? utils.loadCertFromPath(options.appCertPath) : utils.loadCertFromContent(
      utils.formatKey(options.appCertContent, 'CERTIFICATE')
    )
    this._privateKey = options.appPrivateKeyPath ? utils.loadPrivateKeyFromPath(options.appPrivateKeyPath) : utils.loadPrivateKeyFromContent(
      utils.formatKey(options.appPrivateKeyContent, 'PRIVATE KEY')
    )
  }

  /**
   * 转换微信tradeType
   * @returns {string}
   * @private
   */
  // get _tradeType () {
  //   switch (this.options.tradeType.toUpperCase()) {
  //     default:
  //     case 'JSAPI':
  //       return 'jsapi'
  //     case 'APP':
  //       return 'app'
  //     case 'NATIVE':
  //       return 'native'
  //     case 'MWEB':
  //       return 'h5'
  //   }
  // }

  /**
   * 生成 Authorization
   * @param {String} method 请求方式
   * @param {String} action URL
   * @param {Object} body 请求体
   * @returns {string}
   * @private
   */
  _getAuthorization (method = 'GET', action = '', body = {}) {
    const nonceStr = utils.getNonceStr()
    const timestamp = Date.parse(new Date()) / 1000
    const signStr = [
      method,
      method === 'GET' && Object.keys(body).length > 0 ? `${action}?${utils.getQueryStr(body)}` : action,
      timestamp,
      nonceStr,
      method === 'GET' ? '' : JSON.stringify(body)
    ].reduce((sign, key) => {
      sign += key + '\n'
      return sign
    }, '')

    const sign = utils.rsaPrivateKeySign(this._privateKey.toPEM(), signStr).toString('base64')
    const certSerialNo = this._cert.serialNumber.toUpperCase()

    return `WECHATPAY2-SHA256-RSA2048 mchid="${this.options.mchId}",nonce_str="${nonceStr}",signature="${sign}",timestamp="${timestamp}",serial_no="${certSerialNo}"`
  }

  /**
   * 请求
   * @param action
   * @param params
   * @param method
   * @param successStatusCode
   * @returns {Promise<*>}
   * @private
   */
  async _request (action, params, method = 'GET', successStatusCode = 200) {
    params = camel2snakeJson(params)

    const authorization = this._getAuthorization(method, action, params)
    let { status, data = {}, headers } = await uniCloud.httpclient.request(`${this._baseURL}${action}`, {
      method,
      data: params,
      headers: {
        Accept: 'application/json',
        'content-type': 'application/json',
        Authorization: authorization
      },
      dataType: 'json',
      timeout: this.options.timeout
    })
    if (status !== successStatusCode) {
      throw new UniCloudError({
        code: data.code,
        message: data.message
      })
    }
    if (!data) data = {}
    // 响应签名验证
    await this._verifyResponseSign(headers, data)

    if (data.appid) data.appId = data.appid
    if (data.mchid) data.mchId = data.mchid

    return snake2camelJson(data)
  }

  /**
   * 请求公共参数
   * @param params
   * @returns {any}
   * @private
   */
  _publicParams (params) {
    const publicParams = {
      appid: this.options.appId,
      mchid: this.options.mchId
    }

    return Object.assign(publicParams, params)
  }

  /**
   * 生成客户端支付参数
   * @param {String} prepayId
   * @param {String} tradeType
   * @returns {{package: string, appid: *, partnerid: (String|*), prepayid, noncestr: string, timestamp: string}|{timeStamp: string, package: string, appId: *, nonceStr: string}}
   * @private
   */
  _getPayParamsByPrepayId (prepayId, tradeType) {
    let requestOptions
    // 请务必注意各个参数的大小写
    switch (tradeType) {
      case 'APP':
        requestOptions = {
          appid: this.options.subAppId ? this.options.subAppId : this.options.appId,
          partnerid: this.options.mchId,
          prepayid: prepayId,
          package: 'Sign=WXPay',
          noncestr: utils.getNonceStr(),
          timestamp: '' + ((Date.now() / 1000) | 0)
        }
        requestOptions.sign = this._clientPaySign(requestOptions, tradeType)
        break
      case 'JSAPI':
      default: {
        const timeStamp = '' + ((Date.now() / 1000) | 0)
        requestOptions = {
          appId: this.options.subAppId ? this.options.subAppId : this.options.appId,
          nonceStr: utils.getNonceStr(),
          package: 'prepay_id=' + prepayId,
          timeStamp
        }
        // signType也需要sign
        requestOptions.signType = 'RSA'
        requestOptions.paySign = this._clientPaySign(requestOptions, tradeType)
        requestOptions.timestamp = timeStamp
        break
      }
    }
    return requestOptions
  }

  /**
   * 生成客户端支付签名
   * @param {Object} params
   * @param {String} tradeType
   * @returns {string}
   * @private
   */
  _clientPaySign (params, tradeType) {
    const signStr = [
      params.appid || params.appId,
      params.timestamp || params.timeStamp,
      params.noncestr || params.nonceStr,
      tradeType === 'JSAPI' ? params.package : params.prepayid
    ].reduce((sign, key) => {
      sign += key + '\n'
      return sign
    }, '')

    return utils.rsaPrivateKeySign(this._privateKey.toPEM(), signStr).toString('base64')
  }

  /**
   * 获取微信支付平台证书
   * 调用频率限制 1000次/s
   * @see https://pay.weixin.qq.com/wiki/doc/apiv3/apis/wechatpay5_1.shtml
   * @returns {Promise<*|*[]>}
   * @private
   */
  async _getPlatformCert () {
    if (this.platformCertificate.length <= 0) {
      const action = '/v3/certificates'
      const { status, data } = await uniCloud.httpclient.request(`${this._baseURL}${action}`, {
        method: 'GET',
        headers: {
          Accept: 'application/json',
          'content-type': 'application/json',
          Authorization: this._getAuthorization('GET', action)
        },
        dataType: 'json',
        timeout: this.options.timeout
      })

      if (status !== 200) {
        throw new Error('request fail')
      }

      this.platformCertificate = data.data?.reduce((res, cert) => {
        // 解密证书
        if (cert.encrypt_certificate) {
          const { nonce, associated_data: associatedData, ciphertext } = cert.encrypt_certificate
          const certContent = utils.decryptCiphertext(ciphertext, this.options.v3Key, nonce, associatedData)
          cert.certificate = utils.loadCertFromContent(certContent)
        }
        res.push(cert)
        return res
      }, []) ?? []
    }

    this.platformCertificate = this.platformCertificate.filter(item => new Date(item.expire_time).getTime() > Date.now())
    return this.platformCertificate[0]
  }

  /**
   * 验证响应签名
   * @param {Object} headers
   * @param {Object} data
   * @returns {Promise<void>}
   * @private
   */
  async _verifyResponseSign (headers, data = {}) {
    // 获取平台证书
    const platformCert = await this._getPlatformCert()

    const {
      'wechatpay-timestamp': wechatPayTimestamp,
      'wechatpay-nonce': wechatPayNonce,
      'wechatpay-signature': wechatPaySignature
    } = headers

    const signStr = [
      wechatPayTimestamp,
      wechatPayNonce,
      Object.keys(data).length ? JSON.stringify(data) : ''
    ].reduce((sign, key) => {
      sign += key + '\n'
      return sign
    }, '')

    const verify = utils.rsaPublicKeyVerifySign(platformCert.certificate.publicKey.toPEM(), signStr, wechatPaySignature)
    if (!verify) throw new Error('response signature verification failed')
  }

  /**
   * 下载账单文件
   * @param {String} fileUrl 下载地址
   * @returns {*}
   * @private
   */
  _downloadFile (fileUrl) {
    // eslint-disable-next-line node/no-deprecated-api
    const urlParser = url.parse(fileUrl)
    return uniCloud.httpclient.request(fileUrl, {
      method: 'GET',
      headers: {
        Accept: 'application/json',
        'content-type': 'application/json',
        Authorization: this._getAuthorization('GET', urlParser.path)
      },
      dataType: 'text',
      timeout: this.options.timeout
    })
  }
}

/**
 * @param {String} options.appId 应用ID
 * @param {String} options.mchId 商户ID
 * @param {String} options.timeout=5000 请求超时时间
 */
class Payment extends PaymentBase {
  /**
   * 生成下单参数
   * @param {String} params.openid
   * @param {String} params.body
   * @param {String} params.outTradeNo
   * @param {Number} params.totalFee
   * @param {String} params.notifyUrl
   * @param {String} params.spbillCreateIp
   * @param {Object} params.sceneInfo
   * @param {String} params.tradeType
   * @returns {Promise<*>}
   */
  async getOrderInfo (params) {
    params = this._publicParams(params)
    params.sceneInfo.payerClientIp = params.sceneInfo.payerClientIp || '127.0.0.1'
    if (params.tradeType !== 'JSAPI') {
      delete params.openid
    }

    if (!params.tradeType) throw new Error('tradeType required')

    const { tradeType, ...rest } = params

    // 兼容微信tradeType
    const orderResult = await this._request(`/v3/pay/transactions/${params.tradeType === 'MWEB' ? 'h5' : params.tradeType.toLowerCase()}`, rest, 'POST')

    if (params.tradeType === 'NATIVE' || params.tradeType === 'MWEB') {
      return orderResult
    }

    if (!orderResult.prepayId) {
      throw new Error(orderResult.errMsg || '获取prepayId失败')
    }

    return this._getPayParamsByPrepayId(orderResult.prepayId, params.tradeType)
  }

  /**
   * 查询订单
   * @param {String} params.transactionId 平台订单号
   * @param {String} params.outTradeNo 商户订单号
   * @returns {Promise<*>}
   */
  async orderQuery (params) {
    const res = await this._request(
      params.transactionId
        ? `/v3/pay/transactions/id/${params.transactionId}`
        : `/v3/pay/transactions/out-trade-no/${params.outTradeNo}`
      , {
        mchid: this.options.mchId
      })

    // 手动计算 应结订单金额 （应结订单金额=订单金额-免充值优惠券金额）
    res.settlementTotalFee = 0
    if (res.promotion_detail?.length > 0) {
      const free = res.promotion_detail.reduce((amount, coupon) => {
        if (coupon.type === 'NOCASH') {
          amount += coupon.amount
        }

        return amount
      }, 0)

      res.settlementTotalFeeres = res.amount.total - free
    }
    return res
  }

  /**
   * 关闭订单
   * @param {String} params.outTradeNo 商户订单号
   * @returns {Promise<*>}
   */
  async closeOrder (params) {
    return await this._request(`/v3/pay/transactions/out-trade-no/${params.outTradeNo}/close`, {
      mchid: this.options.mchId
    }, 'POST', 204)
  }

  /**
   * 申请退款
   * @param {String} params.outTradeNo 商户订单号
   * @param {String} params.transactionId 平台订单号
   * @param {String} params.outRefundNo 商户退款单号
   * @param {String} params.totalFee 订单总金额
   * @param {String} params.refundFee 退款总金额
   * @param {String} params.refundFeeType 货币种类
   * @param {String} params.refundDesc 退款原因
   * @param {String} params.notifyUrl 退款通知 url
   * @returns {Promise<*>}
   */
  async refund (params) {
    return await this._request('/v3/refund/domestic/refunds', params, 'POST')
  }

  /**
   * 单笔退款查询
   * @description 响应与v2差异: 增加字段: refundId(平台退款单号)，缺少字段：refundDesc(退款理由)，refundList(分笔退款信息)
   * @param {String} params.outTradeNo 商户订单号
   * @returns {Promise<*>}
   */
  async refundQuery (params) {
    return await this._request(`/v3/refund/domestic/refunds/${params.outRefundNo}`)
  }

  /**
   * 申请交易账单
   * @param {String} params.billDate 下载对账单的日期，格式：2014-06-03
   * @param {String} params.billType
   * @returns {Promise<*>}
   */
  async downloadBill (params) {
    return this._request('/v3/bill/tradebill', params).then(res => {
      return this._downloadFile(res.downloadUrl)
    }).then(response => {
      return Promise.resolve({
        content: response.data
      })
    })
  }

  /**
   * 申请资金账单
   * @description 请求与V2差异：billDate(下载对账单的日期) 格式变更为 2014-06-03
   * @param {String} params.billDate 下载对账单的日期，格式：2014-06-03
   * @param {String} params.accountType
   * @returns {Promise<*>}
   */
  async downloadFundflow (params) {
    return this._request('/v3/bill/fundflowbill', params).then(res => {
      return this._downloadFile(res.downloadUrl)
    }).then(response => {
      return Promise.resolve({
        content: response.data
      })
    })
  }

  /**
   * 获取通知类型
   * @param event
   * @returns {String}
   */
  async checkNotifyType (event) {
    const { headers } = event
    const body = typeof event.body === 'string' ? JSON.parse(event.body) : event.body
    // 请求合法验证
    await this._verifyResponseSign(headers, body)

    const { resource } = body

    switch (resource?.original_type) {
      case 'transaction':
      default:
        return 'payment'
      case 'refund':
        return 'refund'
    }
  }

  /**
   * 支付成功回调通知
   * @param event
   * @returns {Promise<*>}
   */
  async verifyPaymentNotify (event) {
    const { headers } = event
    const body = typeof event.body === 'string' ? JSON.parse(event.body) : event.body

    // 请求合法验证
    await this._verifyResponseSign(headers, body)

    const { resource } = body
    const decrypted = utils.decryptCiphertext(resource.ciphertext, this.options.v3Key, resource.nonce, resource.associated_data)

    return snake2camelJson(JSON.parse(decrypted))
  }

  /**
   * 退款回调通知
   * @param event
   * @returns {Promise<*>}
   */
  async verifyRefundNotify (event) {
    const { headers } = event
    const body = typeof event.body === 'string' ? JSON.parse(event.body) : event.body

    // 请求合法验证
    await this._verifyResponseSign(headers, body)

    const { resource } = body
    const decrypted = utils.decryptCiphertext(resource.ciphertext, this.options.v3Key, resource.nonce, resource.associated_data)

    return snake2camelJson(JSON.parse(decrypted))
  }
}

export default Payment
